package uk.ac.jisc.depositmo.ui.cocoa;

/**
 * Copyright (c) 2011, University of Southampton
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without modification, 
 * are permitted provided that the following conditions are met:
 * # Redistributions of source code must retain the above copyright notice, 
 *     this list of conditions and the following disclaimer.
 * # Redistributions in binary form must reproduce the above copyright notice, 
 *     this list of conditions and the following disclaimer in the documentation 
 *     and/or other materials provided with the distribution.
 * # Neither the name of the University of Southampton nor the names of its
 *     contributors may be used to endorse or promote products derived from this
 *     software without specific prior written permission.
 *  
 *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 *  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 *  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 
 *  ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 
 *  LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 
 *  DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 
 *  LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 
 *  ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 
 *  (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 
 *  SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

import java.util.HashMap;
import java.util.Map;

import org.eclipse.swt.SWT;
import org.eclipse.swt.internal.C;
import org.eclipse.swt.internal.Callback;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Listener;

/**
 * Provides base for accessing native Cocoa functionality (if available as the target platform.)
 * @author Mark Borkum <mark.borkum@gmail.com>
 *
 */
public abstract class AbstractCocoaUIEnhancer {
	public static final boolean OK = SWT.getPlatform().equals("cocoa");
	
	private static final Map<String, Class<?>> CLASS_FOR_NAME = new HashMap<String, Class<?>>();

	protected static Class<?> classForName(String className) {
		if (CLASS_FOR_NAME.containsKey(className)) {
			return CLASS_FOR_NAME.get(className);
		} else {
			try {
				Class<?> klass = Class.forName(className);

				CLASS_FOR_NAME.put(className, klass);

				return klass;
			} catch (ClassNotFoundException e) {
				throw new IllegalStateException(e);
			}
		}
	}
	
	private HookObject hookObject;

	private long objc_lookUpClass_;

	public AbstractCocoaUIEnhancer(final Display display) {
		this.hookObject = new HookObject(display);

		this.objc_lookUpClass_ = objc_lookUpClass(classForName("org.eclipse.swt.internal.cocoa.OS"), "SWTApplicationDelegate");
	}
	
	public Display getDisplay() {
		return getHookObject().getDisplay();
	}
	
	protected HookObject getHookObject() {
		return hookObject;
	}
	
	protected long objc_lookUpClass() {
		return objc_lookUpClass_;
	}

	protected void objc_class_addMethod3(long sel) {
		objc_class_addMethod(sel, getHookObject().getCallback3Address(), "@:@");
	}
	
	protected void objc_class_addMethod4(long sel) {
		objc_class_addMethod(sel, getHookObject().getCallback4Address(), "@:@:@");
	}
	
	protected void objc_class_addMethod4c(long sel) {
		objc_class_addMethod(sel, getHookObject().getCallback4Address(), "@:@c");
	}
	
	protected void objc_class_addMethod(long sel, long callback_PTR, String signature) {
		invoke(classForName("org.eclipse.swt.internal.cocoa.OS"), "class_addMethod", new Object[] {
			wrapPointer(objc_lookUpClass()),
			wrapPointer(sel),
			wrapPointer(callback_PTR),
			signature
		}); //$NON-NLS-1$
	}

	protected Object objc_NSMenu_itemAtIndex(Object menu, long index) {
		return invoke(classForName("org.eclipse.swt.internal.cocoa.NSMenu"), menu, "itemAtIndex", new Object[] {
			wrapPointer(index)	
		});
	}

	protected void objc_NSMenuItem_setAction(Object menuItem, long sel, Listener listener) {
		if (!getHookObject().hasListener(sel)) {
			objc_class_addMethod3(sel);

			objc_NSMenuItem_setAction(menuItem, sel);
		}

		getHookObject().setListener(sel, listener);
	}

	protected void objc_NSMenuItem_setAction(Object menuItem, long sel) {
		invoke(classForName("org.eclipse.swt.internal.cocoa.NSMenuItem"), menuItem, "setAction", new Object[] {
			wrapPointer(sel)	
		});
	}

	protected void objc_NSMenuItem_setEnabled(Object menuItem, boolean enabled) {
		invoke(classForName("org.eclipse.swt.internal.cocoa.NSMenuItem"), menuItem, "setEnabled", new Object[] { enabled });
	}

	protected void objc_NSMenuItem_setTitle(Object menuItem, String title) {
		invoke(classForName("org.eclipse.swt.internal.cocoa.NSMenuItem"), menuItem, "setTitle", new Object[] { 
			objc_NSString_stringWith(title)
		});
	}

	protected Object objc_NSString_stringWith(String string) {
		return invoke(classForName("org.eclipse.swt.internal.cocoa.NSString"), "stringWith", new Object[] { string });
	}

	protected static long objc_lookUpClass(Class<?> klass, String objc_className) {
		return convertToLong(invoke(klass, "objc_lookUpClass", new Object[] { objc_className }));
	}

	protected static long sel_registerName(Class<?> klass, String objc_methodName) {
		return convertToLong(invoke(klass, "sel_registerName", new Object[] { objc_methodName }));
	}

	protected static long convertToLong(Object object) {
		if (object instanceof Integer) {
			return ((Integer) object).longValue();
		} else if (object instanceof Long) {
			return ((Long) object).longValue();
		} else {
			return 0L;
		}
	}

	protected static Object wrapPointer(long value) {
		final Class<?> PTR_CLASS = (C.PTR_SIZEOF == 8) ? long.class : int.class;

		if (PTR_CLASS == long.class) {
			return new Long(value);
		} else {
			return new Integer((int) value);
		}
	}

	protected static Object invoke(Class<?> klass, String methodName, Object[] arguments) {
		return invoke(klass, null, methodName, arguments);
	}

	protected static Object invoke(Class<?> klass, Object object, String methodName, Object[] arguments) {
		try {
			Class<?>[] signature = new Class<?>[arguments.length];

			for ( int i = 0; i < arguments.length; i++ ) {
				Class<?> argumentClass = arguments[i].getClass();

				if (argumentClass == Integer.class) {
					signature[i] = int.class;
				} else if (argumentClass == Long.class) {
					signature[i] = long.class;
				} else if (argumentClass == Byte.class) {
					signature[i] = byte.class;
				} else if (argumentClass == Boolean.class) {
					signature[i] = boolean.class;
				} else {
					signature[i] = argumentClass;
				}
			}

			return klass.getMethod(methodName, signature).invoke(object, arguments);
		} catch (Exception e) {
			throw new IllegalStateException(e);
		}
	}

	protected static Object invoke(Class<?> klass, String methodName) {
		return invoke(klass, methodName, (Class<?>[]) null, (Object[]) null);
	}

	protected static Object invoke(Object object, String methodName) {
		return invoke(object, methodName, (Class<?>[]) null, (Object[]) null);
	}

	protected static Object invoke(Class<?> klass, String methodName, Class<?>[] argumentTypes, Object... arguments) {
		return invoke(klass, null, methodName, argumentTypes, arguments);
	}

	protected static Object invoke(Object object, String methodName, Class<?>[] argumentTypes, Object... arguments) {
		return invoke(object.getClass(), object, methodName, argumentTypes, arguments);
	}

	protected static Object invoke(Class<?> klass, Object object, String methodName, Class<?>[] argumentTypes, Object... arguments) {
		try {
			return klass.getDeclaredMethod(methodName, argumentTypes).invoke(object, arguments);
		} catch (Exception e) {
			throw new IllegalStateException(e);
		}
	}

	protected static class HookObject {
		private Display display;

		private long callback3_PTR, callback4_PTR;

		private Map<Long, Listener> selToListenerMap = new HashMap<Long, Listener>();

		public HookObject(final Display display) {
			this.setDisplay(display);

			final Callback callback3 = new Callback(this, "actionProc", 3); //$NON-NLS-1$
			
			final Callback callback4 = new Callback(this, "actionProc", 4); //$NON-NLS-1$

			display.disposeExec(new Runnable() {
				public void run() {
					invoke(callback3, "dispose");
					invoke(callback4, "dispose");
				}
			});

			long callback3_PTR = 0;

			try {
				callback3_PTR = convertToLong(Callback.class.getMethod("getAddress", new Class[0]).invoke(callback3, (Object[]) null));
			} catch (Exception e) {
				throw new IllegalStateException(e);
			}

			if (callback3_PTR == 0) {
				SWT.error(SWT.ERROR_NO_MORE_CALLBACKS);
			}

			this.callback3_PTR = callback3_PTR;
			
			long callback4_PTR = 0;

			try {
				callback4_PTR = convertToLong(Callback.class.getMethod("getAddress", new Class[0]).invoke(callback4, (Object[]) null));
			} catch (Exception e) {
				throw new IllegalStateException(e);
			}

			if (callback4_PTR == 0) {
				SWT.error(SWT.ERROR_NO_MORE_CALLBACKS);
			}

			this.callback4_PTR = callback4_PTR;
		}

		public Display getDisplay() {
			return display;
		}
		
		private void setDisplay(Display display) {
			this.display = display;
		}

		public long getCallback3Address() {
			return callback3_PTR;
		}
		
		public long getCallback4Address() {
			return callback4_PTR;
		}

		public Listener getListener(long sel) {
			return this.selToListenerMap.get(Long.valueOf(sel));
		}

		public boolean hasListener(long sel) {
			return this.selToListenerMap.containsKey(Long.valueOf(sel));
		}

		public void setListener(long sel, Listener listener) {
			this.selToListenerMap.put(Long.valueOf(sel), listener);
		}

		/**
		 * 32-bit 
		 */
		public int actionProc(int id, int sel, int arg0) {
			return (int) actionProc((long) id, (long) sel, (long) arg0);
		}
		
		/**
		 * 32-bit 
		 */
		public int actionProc(int id, int sel, int arg0, int arg1) {
			return (int) actionProc((long) id, (long) sel, (long) arg0, (long) arg1);
		}

		/**
		 * 64-bit
		 */
		public long actionProc(long id, long sel, long arg0) {
//			System.out.println("actionProc(" + id + ", " + sel + ", " + arg0 + ")");
			
			Listener listener = getListener(Long.valueOf(sel));

			if (listener != null) {
				Event e = new Event();
				e.data = new Long[] { arg0 };
				
				listener.handleEvent(e);
			}

			// Return value is never read locally.
			return 99L;
		}
		
		/**
		 * 64-bit
		 */
		public long actionProc(long id, long sel, long arg0, long arg1) {
//			System.out.println("actionProc(" + id + ", " + sel + ", " + arg0 + ", " + arg1 + ")");
			
			Listener listener = getListener(Long.valueOf(sel));

			if (listener != null) {
				Event e = new Event();
				e.data = new Long[] { arg0, arg1 };
				
				listener.handleEvent(e);
			}

			// Return value is never read locally.
			return 99L;
		}
	}
}
